package pers.tetris;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * 模型类，定义所有的处理逻辑
 */
public class TetrisModel {
    /**
     * 地图的行数
     */
    public static final int ROWS = 20;
    /**
     * 地图的列数
     */
    public static final int COLS = 10;
    /**
     * 一次性缓存多少个Shape
     * @see TetrisModel#holds
     */
    public static final int SHAPE_BUFFER_SIZE = 6;
    /**
     * 可用的颜色总数
     */
    public static final int COLOR_SIZE = 7;

    /**
     * 灵敏度，按住按键不放时，多久重复一次动作，单位ms
     */
    int sensitivity = 1000;
    /**
     * 方块下落速度，多久下落一次，单位ms
     */
    int speed = 3000;

    /**
     * 0 - empty; other - the color
     */
    private int[][] map = new int[ROWS][COLS];

    /**
     * 当前操作的形状对象
     */
    private Shape currentShape;
    /**
     * 当前操作的形状指定的颜色
     * 范围: 0<= shapeColor <COLOR_SIZE
     * @see TetrisModel#COLOR_SIZE
     */
    private int shapeColor;

    /**
     * 接下来的形状列表
     */
    public ArrayList<Shape> holds = new ArrayList<>();

    /**
     * 形状的生成器
     */
    public Supplier<Shape> shapeGenerator;

    /**
     * 当前的分数
     */
    public int score;
    /**
     * 是否游戏结束
     */
    public boolean gameOver = false;
    /**
     * 是否游戏暂停
     */
    public boolean paused = true;
    /**
     * model 改变的监听列表
     * 当model变化时，会依次通知这些监听对象
     * @see TetrisModel#addModelListener(Consumer)
     * @see TetrisModel#removeModelListener(Consumer)
     * @see TetrisModel#fireModelChange()
     */
    public Set<Consumer<TetrisModel>> modelListeners = new HashSet<>();
    /**
     * 游戏结束的通知列表
     * @see TetrisModel#addGameOverListener(Runnable)
     * @see TetrisModel#removeGameOverListener(Runnable)
     * @see TetrisModel#fireGameOver()
     */
    public Set<Runnable> gameOverListeners = new HashSet<>();

    public TetrisModel(Supplier<Shape> shapeGenerator) {
        this.shapeGenerator = shapeGenerator;

        initMap();
    }

    /**
     * 初始化地图：清空当前map,取出Shape等
     */
    public void initMap() {
        for (int i = 0; i < ROWS; i++) {
            Arrays.fill(map[i],0);
        }
        score = 0;
        gameOver = false;
        paused = true;

        currentShape = shapeGenerator.get();
        fillHold();
        nextShape();
    }

    /**
     * 依次向缓存中添加Shape,直到缓存满了
     * @see TetrisModel#holds
     * @see TetrisModel#SHAPE_BUFFER_SIZE
     */
    private void fillHold(){
        while (holds.size()<SHAPE_BUFFER_SIZE){
            holds.add(shapeGenerator.get());
        }
    }

    /**
     * 通知外界 model 发生了变化
     */
    private void fireModelChange(){
        for (Consumer<TetrisModel> modelListener : modelListeners) {
            modelListener.accept(this);
        }
    }

    /**
     * 通知外界 游戏结束了
     */
    private void fireGameOver(){
        for (Runnable listener: gameOverListeners) {
            listener.run();
        }
    }

    /**
     * 添加Shape到游戏中
     */
    private void nextShape(){
        // 取出Shape缓存中第一个元素
        this.currentShape = holds.remove(0);
        shapeColor ++;

        // 判断游戏是否以结束（Shape放不下了）
        if (map[0][3]>0 || map[0][4]>0|| map[0][5]>0|| map[0][6]>0){
            gameOver = true;
            fireGameOver();
            return;
        }
        // 判断新Shape是否与map碰撞，如果碰撞则游戏结束
//        if (currentShape.borderTest(map,currentShape.getX(),currentShape.getY())){
//            gameOver = true;
//            fireGameOver();
//            return;
//        }

        // 填满缓存
        fillHold();
        // 通知model变化
        fireModelChange();
    }

    /**
     * 形状定时下落，tick tick ...
     */
    public void onTick(){
        if (gameOver||paused){
            return;
        }
        // 自动下移
        // 碰撞检测
        moveDown();
    }

    /**
     * 形状右移
     */
    public void moveRight() {
        if (gameOver||paused){
            return;
        }

        if (!currentShape.borderTest(map,currentShape.getX()+1,currentShape.getY())){
            // 没有碰撞，可以右移
            currentShape.setX(currentShape.getX()+1);
            fireModelChange();
        }
    }

    /**
     * 形状左移
     */
    public void moveLeft() {
        if (gameOver||paused){
            return;
        }

        if (!currentShape.borderTest(map,currentShape.getX()-1,currentShape.getY())){
            // 没有碰撞，可以左移
            currentShape.setX(currentShape.getX()-1);
            fireModelChange();
        }
    }

    /**
     * 形状向下移动
     */
    public void moveDown() {
        if (gameOver||paused){
            return;
        }

        if (!currentShape.borderTest(map,currentShape.getX(),currentShape.getY()+1)){
            currentShape.setY(currentShape.getY()+1);
            fireModelChange();
        } else {
            // 发生碰撞 , 先检测是否已Game Over
            // 同时合并shape 到 map, 然后创建下一个shape
            int[][] shape = currentShape.getShape();
            for (int i = 0; i < shape.length; i++) { // y
                for (int j = 0; j < shape[i].length; j++) { //x
                    int t = shape[i][j];
                    if (t>0){
                        // 无法下落，且当前形状超出地图上边界，游戏结束
                        if (i+currentShape.getY()<0){
                            gameOver = true;
                            fireGameOver();
                            return;
                        }
                        // 把形状转化成地图上的块
                        map[i+currentShape.getY()][j+ currentShape.getX()] += t *(1+(shapeColor % COLOR_SIZE));
                    }
                }
            }

            // 自下而上检测行是否满了，进行消除
            for (int i = 0; i < ROWS;) {
                // 检测自下而上的第i行
                if (Arrays.stream(map[ROWS-i-1]).allMatch(x->x>0)){
                    for (int j = (ROWS - i - 1); j > 0; j--) {
                        map[j] = map[j-1];
                    }
                    map[0] = new int[COLS];
                    score ++;
                }else {
                    i++;
                }
            }
            nextShape();
        }
    }

    /**
     * 旋转形状
     */
    public void ratoteShape() {
        if (gameOver||paused){
            return;
        }
        // 先旋转
        currentShape.rotate();
        // 碰撞则再转回去
        if (currentShape.borderTest(map,currentShape.getX(),currentShape.getY())){
            if (!currentShape.borderTest(map, currentShape.getX()-1, currentShape.getY())){
                // 虽然碰撞了，但左移一格还可以放得下
                currentShape.setX(currentShape.getX()-1);
            }
            else if (!currentShape.borderTest(map, currentShape.getX()+1, currentShape.getY())) {
                // 虽然碰撞了，但右移一格还可以放得下
                currentShape.setX(currentShape.getX()+1);
            }
            else {
                // 只好转回去了
                if(currentShape instanceof FixedShape){
                    ((FixedShape) currentShape).rotateBack();
                }
                else {
                    currentShape.rotate();
                    currentShape.rotate();
                    currentShape.rotate();
                }
            }
        }
        fireModelChange();
    }

    public int[][] getMap() {
        return map;
    }

    public Shape getCurrent() {
        return currentShape;
    }

    public int getShapeColor() {
        return shapeColor % COLOR_SIZE + 1;
    }

    public List<Shape> getHolds() {
        return holds;
    }

    public int getScore() {
        return score;
    }

    public void addModelListener(Consumer<TetrisModel> listener){
        this.modelListeners.add(listener);
    }

    public boolean removeModelListener(Consumer<TetrisModel> listener){
        return this.modelListeners.remove(listener);
    }

    public void addGameOverListener(Runnable listener){
        gameOverListeners.add(listener);
    }

    public boolean removeGameOverListener(Runnable listener){
        return gameOverListeners.remove(listener);
    }

    public int getSensitivity() {
        return sensitivity;
    }

    public void setSensitivity(int sensitivity) {
        this.sensitivity = sensitivity;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

    public boolean isPaused() {
        return paused;
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
    }
}
